#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gobang.h"

#define kWIN_CRITERION_FOR_COUNT 5

typedef struct {
    int dx;
    int dy;
}Direction;


const Direction vertical_check_direction = {.dx = 1, .dy = 0};
const Direction horizontal_check_direction = {.dx = 0, .dy = 1};
const Direction diagonal_check_direction = {.dx = 1, .dy = 1};
const Direction backdiagonal_check_direction = {.dx = 1, .dy = -1};


int Gobang_ParseGobangCoordinatesFromString(char *str, GobangCoordinates *gobang_coord)
{
    if (strlen(str) != 2 && strlen(str) != 3) {
        return kGOBANG_CODE_OTHER_WRONG;
    }

    gobang_coord->letter = str[0];
    gobang_coord->number = atoi(str + 1);

    return kGOBANG_CODE_OK;
}


static void initializeBoard(ChessBoard *chess_board)
{
    memset(chess_board, kCHESS_SIDE_EMPTY, kBOARD_BOUND_DOWN * kBOARD_BOUND_RIGHT);
}


ChessBoard *Gobang_NewChessBoard()
{
    ChessBoard *chess_board = malloc(sizeof(ChessBoard));
    initializeBoard(chess_board);

    return chess_board;
}


void Gobang_DestoryChessBoard(ChessBoard *chess_board)
{
    free(chess_board);
}


char Gobang_GetChessByBoardCoordinates(ChessBoard *chess_board, BoardCoordinates board_coord)
{
    return (*chess_board)[board_coord.x][board_coord.y];
}


static void setChessByBoardCoordinates(ChessBoard *chess_board, BoardCoordinates board_coord, char side)
{
    (*chess_board)[board_coord.x][board_coord.y] = side;
}


static int boardCoordinatesIsOutOfRange(BoardCoordinates board_coord)
{
    return board_coord.x >= kBOARD_BOUND_DOWN || 
            board_coord.y >= kBOARD_BOUND_RIGHT || 
            board_coord.x <= kBOARD_BOUND_UP || 
            board_coord.y <= kBOARD_BOUND_LEFT;
}


static int positionHasChessAlready(ChessBoard *chess_board, BoardCoordinates board_coord)
{
    char chess = Gobang_GetChessByBoardCoordinates(chess_board, board_coord);

    return chess != kCHESS_SIDE_EMPTY;
}


static BoardCoordinates parseBoardCoordinatesFromGobangCoordinates(GobangCoordinates gobang_coord)
{
    BoardCoordinates board_coord = {
        .x = gobang_coord.number, 
        .y = gobang_coord.letter - kGOBANG_COORDINATES_LETTER_START + 1
    };

    return board_coord;
}


int Gobang_TryPuttingChess(ChessBoard *chess_board, char chess_side, GobangCoordinates gobang_coord)
{
    BoardCoordinates board_coord = parseBoardCoordinatesFromGobangCoordinates(gobang_coord);

    if (chess_side != kCHESS_SIDE_BLACK && chess_side != kCHESS_SIDE_WHITE) {
        return kGOBANG_CODE_WRONG_SIDE;
    }
    
    if (boardCoordinatesIsOutOfRange(board_coord)) {
        return kGOBANG_CODE_OUT_OF_BOARD;
    }

    if (positionHasChessAlready(chess_board, board_coord)) {
        return kGOBANG_CODE_ALREADY_PUT;
    }
    
    setChessByBoardCoordinates(chess_board, board_coord, chess_side);

    return kGOBANG_CODE_OK;
}


static Direction inverseDirection(Direction origin_direction)
{
    Direction inv_direction = {
        .dx = - origin_direction.dx,
        .dy = - origin_direction.dy
    };

    return inv_direction;
}


static void cursorStep(BoardCoordinates *cursor, Direction step_direction)
{
    cursor->x += step_direction.dx;
    cursor->y += step_direction.dy;
}


static void moveCursorToExcludeLastChessman(BoardCoordinates *cursor, Direction check_direction)
{
    cursorStep(cursor, check_direction);
}


static int continuousCountWithLastChessmanExcludedInOneDirection(ChessBoard *chess_board, BoardCoordinates last_coord, Direction check_direction)
{
    char chess;
    unsigned int count = 0;
    BoardCoordinates cursor = last_coord;
    char last_side = Gobang_GetChessByBoardCoordinates(chess_board, last_coord);
    
    moveCursorToExcludeLastChessman(&cursor, check_direction);
    while(!boardCoordinatesIsOutOfRange(cursor)) {
        chess = Gobang_GetChessByBoardCoordinates(chess_board, cursor);
        
        if (chess != last_side) {
            break;
        } else {
            count++;
        }

        cursorStep(&cursor, check_direction);
    }

    return count;
}


static int continuousCountWithLastChessmanExcludedInInverseDirection(ChessBoard *chess_board, BoardCoordinates last_coord, Direction check_direction)
{
    return continuousCountWithLastChessmanExcludedInOneDirection(chess_board, last_coord, inverseDirection(check_direction));
}


static int checkChessBoardInOneLine(ChessBoard *chess_board, BoardCoordinates last_coord, Direction check_direction)
{
    const int count_myself = 1;
    int count_in_one_way = continuousCountWithLastChessmanExcludedInOneDirection(chess_board, last_coord, check_direction);
    int count_in_other_way = continuousCountWithLastChessmanExcludedInInverseDirection(chess_board, last_coord, check_direction);

    return (count_in_one_way + count_in_other_way + count_myself) >= kWIN_CRITERION_FOR_COUNT ? kGOBANG_CODE_ALREADY_WON : kGOBANG_CODE_NO_ONE_WIN;
}


static int verticallyCheckChessBoard(ChessBoard *chess_board, BoardCoordinates last_coord)
{
    return checkChessBoardInOneLine(chess_board, last_coord, vertical_check_direction);
}


static int horizontallyCheckChessBoard(ChessBoard *chess_board, BoardCoordinates last_coord)
{
    return checkChessBoardInOneLine(chess_board, last_coord, horizontal_check_direction);
}


static int diagonallyCheckChessBoard(ChessBoard *chess_board, BoardCoordinates last_coord)
{
    return checkChessBoardInOneLine(chess_board, last_coord, diagonal_check_direction);
}


static int backdiagonallyCheckChessBoard(ChessBoard *chess_board, BoardCoordinates last_coord)
{
    return checkChessBoardInOneLine(chess_board, last_coord, backdiagonal_check_direction);
}


int Gobang_CheckGameWinOrNot(ChessBoard *chess_board, GobangCoordinates last_gobang_coord)
{
    BoardCoordinates last_coord = parseBoardCoordinatesFromGobangCoordinates(last_gobang_coord);

    if (boardCoordinatesIsOutOfRange(last_coord)) {
        return kGOBANG_CODE_OUT_OF_BOARD;
    }

    if (verticallyCheckChessBoard(chess_board, last_coord) == kGOBANG_CODE_ALREADY_WON ||
        horizontallyCheckChessBoard(chess_board, last_coord) == kGOBANG_CODE_ALREADY_WON ||
        diagonallyCheckChessBoard(chess_board, last_coord) == kGOBANG_CODE_ALREADY_WON ||
        backdiagonallyCheckChessBoard(chess_board, last_coord) == kGOBANG_CODE_ALREADY_WON)
    {
        return kGOBANG_CODE_ALREADY_WON;
    }

    return kGOBANG_CODE_NO_ONE_WIN;
}


void Gobang_GobangCodeToString(int gobang_code)
{
    switch(gobang_code) {
        case kGOBANG_CODE_OK:
            // printf("Code ok.\n");
            break;
        case kGOBANG_CODE_ALREADY_PUT:
            printf("Position want to put chess had chess already.\n");
            break;
        case kGOBANG_CODE_OUT_OF_BOARD:
            printf("Position want to put chess is out of board.\n");
            break;
        case kGOBANG_CODE_WRONG_SIDE:
            printf("Chess want to put is wrong side.\n");
            break;
        case kGOBANG_CODE_ALREADY_WON:
            printf("The game already has a winner.\n");
            break;
        default:
            printf("I don't know what is going on.\n");
    }
}